package db

import (
	"context"
	"fmt"
	"os"
	"time"

	"core/contract/db"
	"core/enum/session"
	"core/model/config"
	modelSession "core/model/session"

	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gopkg.in/natefinch/lumberjack.v2"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"gorm.io/gorm/schema"
)

type MysqlSvc struct {
	db    *gorm.DB
	model db.IDbModel
}

var logLevels = map[zapcore.Level]string{
	zapcore.DebugLevel: "debug",
	zapcore.InfoLevel:  "info",
	zapcore.WarnLevel:  "warn",
	zapcore.ErrorLevel: "error",
	zapcore.PanicLevel: "panic",
}
var dbContext context.Context

func (m MysqlSvc) Add(entry db.IDbModel) error {
	account, ok := dbContext.Value(string(session.SessionAccount)).(modelSession.Account)
	if ok {
		entry.SetCreatedBy(account.ID)
		entry.SetUpdatedBy(account.ID)
	}

	now := time.Now().Unix()
	entry.SetCreatedAt(now)
	entry.SetUpdatedAt(now)

	return m.db.Create(entry).Error
}

func (m MysqlSvc) Remove(entry db.IDbModel) error {
	return m.db.Delete(&entry).Error
}

func (m MysqlSvc) Query(entry db.IDbModel) db.IDb {
	m.db = m.db.Model(&entry)
	return m
}

func (m MysqlSvc) Save(entry db.IDbModel) error {
	account, ok := dbContext.Value(string(session.SessionAccount)).(modelSession.Account)
	if ok {
		entry.SetUpdatedBy(account.ID)
	}

	now := time.Now().Unix()
	entry.SetUpdatedAt(now)

	return m.db.Updates(entry).Error
}

func (m MysqlSvc) Count() (int64, error) {
	var count int64
	err := m.db.Count(&count).Error
	return count, err
}

func (m MysqlSvc) Order(fields ...string) db.IDb {
	m.db = m.db.Order(fields)
	return m
}

func (m MysqlSvc) Select(args ...interface{}) db.IDb {
	m.db = m.db.Select(args[0], args[1:]...)
	return m
}

func (m MysqlSvc) Offset(v int) db.IDb {
	m.db = m.db.Offset(v)
	return m
}

func (m MysqlSvc) Limit(v int) db.IDb {
	m.db = m.db.Limit(v)
	return m
}

func (m MysqlSvc) Where(args ...interface{}) db.IDb {
	m.db = m.db.Where(args[0], args[1:]...)
	return m
}

func (m MysqlSvc) Take(dst interface{}) error {
	return m.db.Take(dst).Error
}

func (m MysqlSvc) Find(dst interface{}) error {
	return m.db.Find(dst).Error
}

func (m MysqlSvc) WithContext(ctx context.Context) {
	dbContext = ctx
}

func newMysqlSvc(config *config.Config) (db.IDb, func(), error) {
	// 创建 gormLogger
	gormLogger, err := newZapGormLogger(config)
	if err != nil {
		return nil, func() {}, err
	}

	// 配置 GORM
	dsn := fmt.Sprintf(
		"%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&timeout=%ds",
		config.Db.Mysql.Default.Username,
		config.Db.Mysql.Default.Password,
		config.Db.Mysql.Default.Ip,
		config.Db.Mysql.Default.Port,
		config.Db.Mysql.Default.DbName,
		config.Db.Mysql.Default.ConnectTimeout,
	)

	mysqlDb, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		NamingStrategy: schema.NamingStrategy{
			SingularTable: true,
		},
		DisableForeignKeyConstraintWhenMigrating: true,       // 禁用自动创建外键约束
		Logger:                                   gormLogger, // 使用自定义 Logger,
	})
	if err != nil {
		return nil, func() {}, err
	}

	sqlDb, err := mysqlDb.DB()
	if err != nil {
		return nil, func() {}, err
	}

	// 设置空闲连接池中链接的最大数量
	sqlDb.SetMaxIdleConns(config.Db.Mysql.Default.MaxIdleConn)
	// 设置打开数据库链接的最大数量
	sqlDb.SetMaxOpenConns(config.Db.Mysql.Default.MaxOpenConn)
	// 设置链接可复用的最大时间
	sqlDb.SetConnMaxLifetime(time.Second * time.Duration(config.Db.Mysql.Default.ConnMaxLifetime))

	return MysqlSvc{
			db: mysqlDb,
		}, func() {
			sqlDb.Close()
		}, nil
}

// zapGormLogger is zap适配器
type zapGormLogger struct {
	zapLogger     *zap.Logger
	slowThreshold time.Duration
}

func (zl *zapGormLogger) LogMode(level logger.LogLevel) logger.Interface {
	return zl
}

func (zl *zapGormLogger) Info(ctx context.Context, msg string, data ...interface{}) {
	zl.zapLogger.Sugar().Infof(msg, data...)
}

func (zl *zapGormLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
	zl.zapLogger.Sugar().Warnf(msg, data...)
}

func (zl *zapGormLogger) Error(ctx context.Context, msg string, data ...interface{}) {
	zl.zapLogger.Sugar().Errorf(msg, data...)
}

func (zl *zapGormLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
	elapsed := time.Since(begin)
	switch {
	case err != nil && zl.zapLogger.Core().Enabled(zap.ErrorLevel):
		sql, rows := fc()
		zl.zapLogger.Error("SQL Trace", zap.Error(err), zap.Duration("elapsed", elapsed), zap.Int64("rows", rows), zap.String("sql", sql))
	case elapsed > zl.slowThreshold && zl.slowThreshold != 0 && zl.zapLogger.Core().Enabled(zap.WarnLevel):
		sql, rows := fc()
		zl.zapLogger.Warn("Slow SQL", zap.Duration("elapsed", elapsed), zap.Int64("rows", rows), zap.String("sql", sql))
	case zl.zapLogger.Core().Enabled(zap.InfoLevel):
		sql, rows := fc()
		zl.zapLogger.Info("SQL Trace", zap.Duration("elapsed", elapsed), zap.Int64("rows", rows), zap.String("sql", sql))
	}
}

func newZapGormLogger(config *config.Config) (*zapGormLogger, error) {
	// 判断目录是否存在
	_, err := os.Stat(config.Db.Mysql.Log.RootDir)
	if os.IsNotExist(err) {
		// 创建根目录
		if err = os.MkdirAll(config.Db.Mysql.Log.RootDir, os.ModePerm); err != nil {
			return nil, err
		}
	}

	// 扩展Zap
	var encoder zapcore.Encoder

	// 调整编码器默认配置
	encoderConfig := zap.NewProductionEncoderConfig()
	encoderConfig.EncodeTime = func(time time.Time, encoder zapcore.PrimitiveArrayEncoder) {
		encoder.AppendString(time.Format("[" + "2006-01-02 15:04:05.000" + "]"))
	}
	encoderConfig.EncodeLevel = func(l zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) {
		encoder.AppendString(string(config.Server.Env) + "." + l.String())
	}

	// 设置编码器
	encoder = zapcore.NewJSONEncoder(encoderConfig)

	var cores []zapcore.Core
	for level, suffix := range logLevels {
		cores = append(cores, zapcore.NewCore(
			encoder,
			zapcore.AddSync(&lumberjack.Logger{
				Filename:   config.Db.Mysql.Log.RootDir + "/" + config.Server.Name + "_" + suffix + ".log",
				MaxSize:    config.Db.Mysql.Log.MaxSize,
				MaxBackups: config.Db.Mysql.Log.MaxBackups,
				MaxAge:     config.Db.Mysql.Log.MaxAge,
				Compress:   config.Db.Mysql.Log.Compress,
			}),
			zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
				return lvl == level
			}),
		))
	}

	return &zapGormLogger{
		zapLogger:     zap.New(zapcore.NewTee(cores...), zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel)),
		slowThreshold: time.Duration(config.Db.Mysql.Log.SlowThreshold) * time.Millisecond, // 默认慢查询阈值
	}, nil
}
